using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using BeeDevelopment.Brazil;

namespace Stetson.Emulator.Hardware
{
    public class T6A04 : IDisposable
    {

        /// <summary>
        /// Represents the width of the LCD RAM in bits.
        /// </summary>
        public const int MemoryWidth = 120;

        /// <summary>
        /// Represents the height of the LCD RAM in rows.
        /// </summary>
        public const int MemoryHeight = 64;

        /// <summary>
        /// Represents the width of the physical LCD in pixels.
        /// </summary>
        public const int LcdWidth = 96;

        /// <summary>
        /// Represents the height of the physical LCD in pixels.
        /// </summary>
        public const int LcdHeight = 64;

        /// <summary>
        /// Represents the "busy" delay.
        /// </summary>
        public const int ClockCyclesBetweenWrites = 60;

        private readonly Z80A cpu;
        /// <summary>
        /// Gets the CPU this LCD is connected to.
        /// </summary>
        public Z80A Cpu
        {
            get { return this.cpu; }
        }

        private byte currentRow;
        /// <summary>
        /// Gets or sets the current row.
        /// </summary>
        public byte CurrentRow
        {
            get { return this.currentRow; }
            set { this.currentRow = value; }
        }


        private byte currentColumn;
        /// <summary>
        /// Gets or sets the current column.
        /// </summary>
        public byte CurrentColumn
        {
            get { return this.CurrentColumn; }
            set { this.CurrentColumn = value; }
        }

        private int wordSize;
        /// <summary>
        /// Gets or sets the number of pixels used in each byte transferred through the data port.
        /// </summary>
        public int WordSize
        {
            get { return this.wordSize; }
            set
            {
                if (value != 6 && value != 8) throw new ArgumentOutOfRangeException("Word size must be 6 or 8 bits.");
                this.wordSize = value;
            }
        }

        /// <summary>
        /// Defines the automatic increment mode.
        /// </summary>
        public enum AutoIncrementModes : byte
        {
            DecrementRow = 0x04,
            IncrementRow = 0x05,
            DecrementColumn = 0x06,
            IncrementColumn = 0x07,
        }


        private AutoIncrementModes autoIncrementMode;
        /// <summary>
        /// Gets or sets the current auto-increment mode.
        /// </summary>
        public AutoIncrementModes AutoIncrementMode
        {
            get { return this.autoIncrementMode; }
            set
            {
                // TODO: XNA compatible?
                if ((int)value < 4 || (int)value > 7) throw new ArgumentOutOfRangeException("Invalid auto-increment mode.");
                this.autoIncrementMode = value;
            }
        }

        private bool enabled;
        /// <summary>
        /// Gets or sets whether the LCD is connected to its RAM.
        /// </summary>
        public bool Enabled
        {
            get { return this.enabled; }
            set { this.enabled = value; }
        }

        private byte zAddress;
        /// <summary>
        /// Gets or sets the row number in memory that appears at the top of the physical LCD.
        /// </summary>
        public byte ZAddress
        {
            get { return this.zAddress; }
            set { this.zAddress = value; }
        }

        // TODO: Change to XNA
        private Bitmap LcdBitmap;
        private bool[] LcdBits;
        private float[] LcdBlendBits;
        private int[] LcdBuffer;
        private Rectangle LcdRectangle;

        private int Contrast;
        private int[,] ContrastingColours;


        private int LastWrittenClockCycles;

        public T6A04(Z80A cpu)
        {

            this.cpu = cpu;

            // TODO: Change to XNA
            this.LcdBitmap = new Bitmap(96, 64);

            this.ContrastingColours = new int[256, 64];
            int r, g, b;
            for (int i = 0; i < 64; ++i)
            {
                for (int j = 0; j < 256; ++j)
                {
                    int BrightLevel = Math.Min(255, Math.Max(0, ((i - 48) * 8) + 128 + ((j) - 128)));

                    r = Math.Min(255, Math.Max(0, (BrightLevel * 0xCC) / 256));
                    g = Math.Min(255, Math.Max(0, (BrightLevel * 0xE0) / 256));
                    b = Math.Min(255, Math.Max(0, (BrightLevel * 0xCF) / 256));

                    this.ContrastingColours[j, i] = r * 0x10000 + g * 0x100 + b;
                }
            }



            this.Reset();
        }

        public void Dispose()
        {
            if (this.LcdBitmap != null) this.LcdBitmap.Dispose();
        }

        public void Reset()
        {
            this.LcdBits = new bool[MemoryWidth * MemoryHeight];
            this.LcdBlendBits = new float[MemoryWidth * MemoryHeight];
            this.LcdBuffer = new int[LcdWidth * LcdHeight];
            this.LcdRectangle = new Rectangle(0, 0, LcdWidth, LcdHeight);
            this.Contrast = 63;
            this.WriteControl(0x04);
        }

        private int MemoryBitAddress
        {
            get
            {
                return (this.currentRow * 120) + ((this.currentColumn * this.wordSize) % 120);
            }
        }

        public void WriteData(byte value)
        {
            this.LastWrittenClockCycles = this.cpu.TotalExecutedCycles;
            value <<= 8 - this.wordSize;
            int WriteOffset = this.MemoryBitAddress;
            for (int i = 0; i < this.wordSize; ++i)
            {
                this.LcdBits[WriteOffset++] = (value & 0x80) != 0;
                value <<= 1;
            }
            this.Increment();
        }

        public void WriteControl(byte value)
        {
            this.LastWrittenClockCycles = this.cpu.TotalExecutedCycles;
            switch (value)
            {
                case 0x00: this.WordSize = 6; break;
                case 0x01: this.WordSize = 8; break;
                case 0x02: this.Enabled = false; break;
                case 0x03: this.Enabled = true; break;
                default:
                    if (value >= 0x80 && value <= 0xBF)
                    {
                        this.currentRow = (byte)(value - 0x80);
                    }
                    else if (value >= 0x20 && value <= 0x3F)
                    {
                        this.currentColumn = (byte)(value - 0x20);
                    }
                    else if (value >= 0x40 && value <= 0x7F)
                    {
                        this.ZAddress = (byte)(value - 0x40);
                    }
                    else if (value > 0xC0)
                    {
                        this.Contrast = value & 0x3F;
                    }
                    else if (value >= 4 && value <= 7)
                    {
                        this.autoIncrementMode = (AutoIncrementModes)value;
                    }
                    break;
            }

        }

        private void Increment()
        {

            int ReadOffset = this.MemoryBitAddress;
            CachedReadData = 0;
            for (int i = 0; i < this.wordSize; ++i)
            {
                CachedReadData <<= 1;
                CachedReadData |= (byte)(this.LcdBits[ReadOffset++] ? 0x01 : 0x00);
            }

            this.currentRow &= 0x3F;
            this.currentColumn &= 0x1F;

            switch (this.autoIncrementMode)
            {
                case AutoIncrementModes.DecrementRow:
                    --this.currentRow;
                    break;
                case AutoIncrementModes.IncrementRow:
                    ++this.currentRow;
                    break;
                case AutoIncrementModes.DecrementColumn:
                    if (this.currentColumn == 0x00)
                    {
                        this.currentColumn = (byte)(this.wordSize == 8 ? 14 : 19);
                    }
                    else
                    {
                        --this.currentColumn;
                    }
                    break;
                case AutoIncrementModes.IncrementColumn:
                    if (this.currentColumn == (this.wordSize == 8 ? 14 : 19))
                    {
                        this.currentColumn = 0;
                    }
                    else
                    {
                        ++this.currentColumn;
                    }
                    break;
            }

            this.currentRow &= 0x3F;
            this.currentColumn &= 0x1F;

        }

        private byte CachedReadData = 0xFF;

        /// <summary>
        /// Reads the driver's data port.
        /// </summary>
        /// <returns>A byte of data.</returns>
        public byte ReadData()
        {
            byte Return = CachedReadData;
            Increment();
            return Return;
        }

        /// <summary>
        /// Reads the driver's control port.
        /// </summary>
        /// <returns>The driver status.</returns>
        public byte ReadControl()
        {
            return (byte)(
                ((this.autoIncrementMode == AutoIncrementModes.IncrementColumn || this.autoIncrementMode == AutoIncrementModes.IncrementRow) ? 0x01 : 0x00) |
                ((this.autoIncrementMode == AutoIncrementModes.IncrementColumn || this.autoIncrementMode == AutoIncrementModes.DecrementColumn) ? 0x02 : 0x00) |
                (this.enabled ? 0x20 : 0x00) |
                (this.wordSize == 8 ? 0x40 : 0x00) |
                (((this.cpu.TotalExecutedCycles - this.LastWrittenClockCycles) < T6A04.ClockCyclesBetweenWrites) ? 0x80 : 0x00)
            );
        }

        public void Blend()
        {
            for (int i = 0; i < this.LcdBlendBits.Length; ++i)
            {
                float Target = this.LcdBits[i] ? 0 : 1;
                this.LcdBlendBits[i] = (this.LcdBlendBits[i] * 4 + Target) / 5f;
            }
        }

        /// <summary>
        /// Gets an image of the LCD output as a Bitmap.
        /// </summary>
        public Bitmap LcdOutput
        {
            get
            {

                Blend();

                if (this.enabled)
                {

                    for (int y = 0, Destination = 0, Source = MemoryWidth * this.ZAddress; y < 64; ++y, Source += 24)
                    {
                        for (int x = 0; x < 96; ++x, ++Source, ++Destination)
                        {
                            this.LcdBuffer[Destination] = ContrastingColours[Math.Min(255, Math.Max(0, (int)(LcdBlendBits[Source % LcdBits.Length] * 255))), this.Contrast];
                        }
                    }
                }
                else
                {
                    for (int i = 0; i < this.LcdBuffer.Length; ++i) LcdBuffer[i] = ContrastingColours[1, 0];
                }

                BitmapData Dump = this.LcdBitmap.LockBits(LcdRectangle, ImageLockMode.WriteOnly, PixelFormat.Format32bppRgb);
                Marshal.Copy(this.LcdBuffer, 0, Dump.Scan0, this.LcdBuffer.Length);
                this.LcdBitmap.UnlockBits(Dump);
                return this.LcdBitmap;
            }
        }

    }
}
